package cn.hamster3.mc.plugin.core.common.util;

import org.jetbrains.annotations.NotNull;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Collections;
import java.util.Stack;

/**
 * 算数表达式求值工具
 * <p>
 * 传入算数表达式，将返回一个浮点值结果
 * <p>
 * 如果计算过程错误，将返回一个NaN
 */
@SuppressWarnings("unused")
public final class Calculator {
    /**
     * 这个单例线程不安全，如果要在异步线程使用，请自行新建一个实例
     */
    public static final Calculator INSTANCE = new Calculator();
    // 默认除法运算精度
    private static final int DEF_DIV_SCALE = 16;

    private final Stack<String> postfixStack = new Stack<>();// 后缀式栈
    private final Stack<Character> operateStack = new Stack<>();// 运算符栈
    private final int[] operaPriority = new int[]{0, 3, 2, 1, -1, 1, 0, 2};// 运用运算符ASCII码-40做索引的运算符优先级

    /**
     * 按照给定的表达式计算
     *
     * @param expression 要计算的表达式例如:5+12*(3+5)/7
     * @return 计算结果
     */
    public double calculate(String expression) {
        Stack<String> resultStack = new Stack<>();
        prepare(expression);
        Collections.reverse(postfixStack);// 将后缀式栈反转
        String firstValue, secondValue, currentValue;// 参与计算的第一个值，第二个值和算术运算符
        while (!postfixStack.isEmpty()) {
            currentValue = postfixStack.pop();
            if (!isOperator(currentValue.charAt(0))) {// 如果不是运算符则存入操作数栈中
                currentValue = currentValue.replace("~", "-");
                resultStack.push(currentValue);
            } else {// 如果是运算符则从操作数栈中取两个值和该数值一起参与运算
                secondValue = resultStack.pop();
                firstValue = resultStack.pop();

                // 将负数标记符改为负号
                firstValue = firstValue.replace("~", "-");
                secondValue = secondValue.replace("~", "-");

                String tempResult = calculate(firstValue, secondValue, currentValue.charAt(0));
                resultStack.push(tempResult);
            }
        }
        return Double.parseDouble(resultStack.pop());
    }

    /**
     * 数据准备阶段将表达式转换成为后缀式栈
     *
     * @param expression 表达式
     */
    private void prepare(String expression) {
        operateStack.push(',');// 运算符放入栈底元素逗号，此符号优先级最低
        char[] arr = expression.toCharArray();
        int currentIndex = 0;// 当前字符的位置
        int count = 0;// 上次算术运算符到本次算术运算符的字符的长度便于或者之间的数值
        char currentOp, peekOp;// 当前操作符和栈顶操作符
        for (int i = 0; i < arr.length; i++) {
            currentOp = arr[i];
            if (isOperator(currentOp)) {// 如果当前字符是运算符
                if (count > 0) {
                    postfixStack.push(new String(arr, currentIndex, count));// 取两个运算符之间的数字
                }
                peekOp = operateStack.peek();
                if (currentOp == ')') {// 遇到反括号则将运算符栈中的元素移除到后缀式栈中直到遇到左括号
                    while (operateStack.peek() != '(') {
                        postfixStack.push(String.valueOf(operateStack.pop()));
                    }
                    operateStack.pop();
                } else {
                    while (currentOp != '(' && peekOp != ',' && compare(currentOp, peekOp)) {
                        postfixStack.push(String.valueOf(operateStack.pop()));
                        peekOp = operateStack.peek();
                    }
                    operateStack.push(currentOp);
                }
                count = 0;
                currentIndex = i + 1;
            } else {
                count++;
            }
        }
        if (count > 1 || (count == 1 && !isOperator(arr[currentIndex]))) {// 最后一个字符不是括号或者其他运算符的则加入后缀式栈中
            postfixStack.push(new String(arr, currentIndex, count));
        }

        while (operateStack.peek() != ',') {
            postfixStack.push(String.valueOf(operateStack.pop()));// 将操作符栈中的剩余的元素添加到后缀式栈中
        }
    }

    /**
     * 判断是否为算术符号
     *
     * @param c 要判断的字符
     * @return 判断结果
     */
    private boolean isOperator(char c) {
        return c == '+' || c == '-' || c == '*' || c == '/' || c == '(' || c == ')';
    }

    /**
     * 利用ASCII码-40做下标去算术符号优先级
     *
     * @param char1 字符1
     * @param char2 字符2
     * @return 如果是peek优先级高于cur，返回true，默认都是peek优先级要低
     */
    private boolean compare(char char1, char char2) {
        return operaPriority[(char2) - 40] >= operaPriority[(char1) - 40];
    }

    /**
     * 按照给定的算术运算符做计算
     *
     * @param value1  第一个值
     * @param value2  第二个值
     * @param operate 算数运算符
     * @return 运算结果
     */
    private String calculate(@NotNull String value1, @NotNull String value2, char operate) {
        String result = "";
        switch (operate) {
            case '+':
                result = String.valueOf(add(value1, value2));
                break;
            case '-':
                result = String.valueOf(sub(value1, value2));
                break;
            case '*':
                result = String.valueOf(mul(value1, value2));
                break;
            case '/':
                result = String.valueOf(div(value1, value2));
                break;
        }
        return result;
    }

    /**
     * 提供精确的加法运算。
     *
     * @param value1 被加数
     * @param value2 加数
     * @return 两个参数的和
     */
    private double add(@NotNull String value1, @NotNull String value2) {
        BigDecimal b1 = new BigDecimal(value1);
        BigDecimal b2 = new BigDecimal(value2);
        return b1.add(b2).doubleValue();
    }

    /**
     * 提供精确的减法运算。
     *
     * @param value1 被减数
     * @param value2 减数
     * @return 两个参数的差
     */
    private double sub(@NotNull String value1, @NotNull String value2) {
        BigDecimal b1 = new BigDecimal(value1);
        BigDecimal b2 = new BigDecimal(value2);
        return b1.subtract(b2).doubleValue();
    }

    /**
     * 提供精确的乘法运算。
     *
     * @param value1 被乘数
     * @param value2 乘数
     * @return 两个参数的积
     */
    private double mul(@NotNull String value1, @NotNull String value2) {
        BigDecimal b1 = new BigDecimal(value1);
        BigDecimal b2 = new BigDecimal(value2);
        return b1.multiply(b2).doubleValue();
    }

    /**
     * 提供（相对）精确的除法运算，当发生除不尽的情况时，精确到 小数点以后10位，以后的数字四舍五入。
     *
     * @param value1 被除数
     * @param value2 除数
     * @return 两个参数的商
     */
    private double div(@NotNull String value1, @NotNull String value2) {
        BigDecimal b1 = new BigDecimal(value1);
        BigDecimal b2 = new BigDecimal(value2);
        return b1.divide(b2, DEF_DIV_SCALE, RoundingMode.HALF_UP).doubleValue();
    }
}
